﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security;
using System.ServiceModel;
using System.Web;
using Jsdl.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

namespace Jsdl.Service
{
	public abstract class JsdlServicesHandlerBase : IHttpHandler
	{
		public virtual bool ShowStackTrackInFaults { get { return false; } }
		public abstract Type GetServiceType(string serviceName);

		public bool IsReusable
		{
			get { return true; }
		}

		public virtual void ProcessRequest(HttpContext context)
		{
			var service = context.Request.Path.Substring(context.Request.Path.LastIndexOf("/") + 1);
			var operation = context.Request.QueryString.Get("operation");

			context.Response.ContentType = "application/json";

			var serviceType = GetServiceType(service);
			if (operation.ToLower() == "jsdl")
			{
				var generator = new JsdlServiceFileGenerator(serviceType);
				context.Response.Write(generator.Generate());
			}
			else
			{
				var svc = Activator.CreateInstance(serviceType);
				var method = serviceType.GetMethod(operation);
				if (method == null)
				{
					WriteException(context, 500, "operation_not_found", "The operation '" + operation + "could not be found. ", "");
					return; 
				}

				if (method.GetCustomAttributes(true).Any(a => a is OperationContractAttribute))
				{
					var input = context.Request.ContentEncoding.GetString(
						context.Request.BinaryRead(context.Request.TotalBytes));

					var i = 0;
					var parameters = new List<object>();

					try
					{
						var array = JArray.Parse(input);
						foreach (var param in method.GetParameters())
						{
							if (param.ParameterType == typeof(DateTime))
								parameters.Add(DateTime.Parse(array[i++].ToString()));
							else if (param.ParameterType == typeof(string))
								parameters.Add(array[i++].ToString());
							else if (param.ParameterType == typeof(bool))
								parameters.Add(bool.Parse(array[i++].ToString()));
							else
								parameters.Add(JsonConvert.DeserializeObject(array[i++].ToString(), param.ParameterType));
						}
					} catch { }

					try
					{
						var result = method.Invoke(svc, parameters.ToArray());
						context.Response.Write(JsonConvert.SerializeObject(result,
							new JsonSerializerSettings
							{
								ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 
								ContractResolver = new CamelCasePropertyNamesContractResolver()
							})); 

						// TODO serialize not loaded entities as NULL (not empty collection) if required
					}
					catch (TargetInvocationException ex)
					{
						if (ex.InnerException is JsdlException)
							WriteException(context, 500, (JsdlException) ex.InnerException);
						else
						{
							WriteException(context, 
								ex.InnerException is SecurityException ? 403 : 500, 
								ex.InnerException is SecurityException ? "security" : ex.InnerException.GetType().Name,
								ex.InnerException.Message,
								ex.InnerException.StackTrace);
						}
					}
				}
			}
		}

		protected void WriteException(HttpContext context, int httpStatusCode, JsdlException exception)
		{
			WriteException(context, httpStatusCode, exception.Code, exception.Message, exception.StackTrace);
		}

		protected void WriteException(HttpContext context, int httpStatusCode, string code, string message, string stackTrace)
		{
			context.Response.StatusCode = httpStatusCode;
			context.Response.Write(JsonConvert.SerializeObject(new
			{
				code = code,
				message = message,
				stackTrace = ShowStackTrackInFaults ? stackTrace : "disabled"
			}));
		}
	}
}
